"use client"; import { publishToPublication } from "actions/publishToPublication"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; import { ActionButton } from "components/ActionBar/ActionButton"; import { PubIcon, PubListEmptyContent, PubListEmptyIllo, } from "components/ActionBar/Publications"; import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; import { AddSmall } from "components/Icons/AddSmall"; import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; import { PublishSmall } from "components/Icons/PublishSmall"; import { useIdentityData } from "components/IdentityProvider"; import { InputWithLabel } from "components/Input"; import { Menu, MenuItem } from "components/Menu"; import { useLeafletDomains, useLeafletPublicationData, } from "components/PageSWRDataProvider"; import { Popover } from "components/Popover"; import { SpeedyLink } from "components/SpeedyLink"; import { useToaster } from "components/Toast"; import { DotLoader } from "components/utils/DotLoader"; import { normalizePublicationRecord } from "src/utils/normalizeRecords"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useState, useMemo, useEffect } from "react"; import { useIsMobile } from "src/hooks/isMobile"; import { useReplicache, useEntity } from "src/replicache"; import { useSubscribe } from "src/replicache/useSubscribe"; import { Json } from "supabase/database.types"; import { useBlocks, useCanvasBlocksWithType, } from "src/hooks/queries/useBlocks"; import * as Y from "yjs"; import * as base64 from "base64-js"; import { YJSFragmentToString } from "src/utils/yjsFragmentToString"; import { BlueskyLogin } from "app/login/LoginForm"; import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; import { AddTiny } from "components/Icons/AddTiny"; import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; import { useLocalPublishedAt } from "components/Pages/Backdater"; export const PublishButton = (props: { entityID: string }) => { let { data: pub } = useLeafletPublicationData(); let params = useParams(); let router = useRouter(); if (!pub) return ; if (!pub?.doc) return ( } label={"Publish!"} onClick={() => { router.push(`/${params.leaflet_id}/publish`); }} /> ); return ; }; const UpdateButton = () => { let [isLoading, setIsLoading] = useState(false); let { data: pub, mutate } = useLeafletPublicationData(); let { permission_token, rootEntity, rep } = useReplicache(); let { identity } = useIdentityData(); let toaster = useToaster(); // Get title and description from Replicache state (same as draft editor) // This ensures we use the latest edited values, not stale cached data let replicacheTitle = useSubscribe(rep, (tx) => tx.get("publication_title"), ); let replicacheDescription = useSubscribe(rep, (tx) => tx.get("publication_description"), ); // Use Replicache state if available, otherwise fall back to pub data const currentTitle = typeof replicacheTitle === "string" ? replicacheTitle : pub?.title || ""; const currentDescription = typeof replicacheDescription === "string" ? replicacheDescription : pub?.description || ""; // Get tags from Replicache state (same as draft editor) let tags = useSubscribe(rep, (tx) => tx.get("publication_tags")); const currentTags = Array.isArray(tags) ? tags : []; // Get cover image from Replicache state let coverImage = useSubscribe(rep, (tx) => tx.get("publication_cover_image"), ); // Get post preferences from Replicache state let postPreferences = useSubscribe(rep, (tx) => tx.get<{ showComments?: boolean; showMentions?: boolean; showRecommends?: boolean; } | null>("post_preferences"), ); // Get local published at from Replicache (session-only state, not persisted to DB) let publishedAt = useLocalPublishedAt((s) => pub?.doc ? s[pub?.doc] : undefined, ); return ( } label={isLoading ? : "Update!"} onClick={async () => { if (!pub) return; setIsLoading(true); let result = await publishToPublication({ root_entity: rootEntity, publication_uri: pub.publications?.uri, leaflet_id: permission_token.id, title: currentTitle, description: currentDescription, tags: currentTags, cover_image: coverImage, publishedAt: publishedAt?.toISOString(), postPreferences, }); setIsLoading(false); mutate(); if (!result.success) { toaster({ content: isOAuthSessionError(result.error) ? ( ) : ( "Failed to publish" ), type: "error", }); return; } // Generate URL based on whether it's in a publication or standalone let docUrl = pub.publications ? `${getPublicationURL(pub.publications)}/${result.rkey}` : `https://leaflet.pub/p/${identity?.atp_did}/${result.rkey}`; toaster({ content: (
{pub.doc ? "Updated! " : "Published! "} See Published Post
), type: "success", }); }} /> ); }; const PublishToPublicationButton = (props: { entityID: string }) => { let { identity } = useIdentityData(); let { permission_token } = useReplicache(); let query = useSearchParams(); let [open, setOpen] = useState(query.get("publish") !== null); let isMobile = useIsMobile(); identity && identity.atp_did && identity.publications.length > 0; let [selectedPub, setSelectedPub] = useState(undefined); let router = useRouter(); let { title, entitiesToDelete } = useTitle(props.entityID); let [description, setDescription] = useState(""); return ( setOpen(o)} side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} className="sm:max-w-sm w-[1000px]" trigger={ } label={"Publish on ATP"} /> } > {!identity || !identity.atp_did ? (
Publish on AT Proto
{ <>
Link a Bluesky account to start
a publishing on AT Proto
}
) : (


{selectedPub !== "looseleaf" && selectedPub && ( )} { if (!selectedPub) return; e.preventDefault(); if (selectedPub === "create") return; // For looseleaf, navigate without publication_uri if (selectedPub === "looseleaf") { router.push( `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, ); } else { router.push( `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, ); } }} > Next{selectedPub === "create" && ": Create Pub!"}
)}
); }; const SaveAsDraftButton = (props: { selectedPub: string | undefined; leafletId: string; metadata: { title: string; description: string }; entitiesToDelete: string[]; }) => { let { mutate } = useLeafletPublicationData(); let { rep } = useReplicache(); let [isLoading, setIsLoading] = useState(false); return ( { if (!props.selectedPub) return; if (props.selectedPub === "create") return; e.preventDefault(); setIsLoading(true); await moveLeafletToPublication( props.leafletId, props.selectedPub, props.metadata, props.entitiesToDelete, ); await Promise.all([rep?.pull(), mutate()]); setIsLoading(false); }} > {isLoading ? : "Save as Draft"} ); }; const PostDetailsForm = (props: { title: string; description: string; setDescription: (d: string) => void; }) => { return (
Post Details
props.setDescription(e.currentTarget.value)} />
); }; const PubSelector = (props: { selectedPub: string | undefined; setSelectedPub: (s: string) => void; publications: { identity_did: string; indexed_at: string; name: string; record: Json | null; uri: string; }[]; }) => { // HEY STILL TO DO // test out logged out, logged in but no pubs, and pubbed up flows return (
Publish to…
{props.publications.length === 0 || props.publications === undefined ? (
Publish as Looseleaf
Publish this as a one off doc to AT Proto
Publish to Publication
Publish your writing to a blog on AT Proto

You don't have any Publications yet.{" "} Create one {" "} to get started!
) : (
props.setSelectedPub("looseleaf")} > Publish as Looseleaf
{props.publications.map((p) => { let pubRecord = normalizePublicationRecord(p.record); return ( props.setSelectedPub(p.uri)} > <> {p.name} ); })}
)}
); }; const PubOption = (props: { selected: boolean; onSelect: () => void; children: React.ReactNode; }) => { return ( ); }; let useTitle = (entityID: string) => { let rootPage = useEntity(entityID, "root/page")[0].data.value; let canvasBlocks = useCanvasBlocksWithType(rootPage).filter( (b) => b.type === "text" || b.type === "heading", ); let blocks = useBlocks(rootPage).filter( (b) => b.type === "text" || b.type === "heading", ); let firstBlock = canvasBlocks[0] || blocks[0]; let firstBlockText = useEntity(firstBlock?.value, "block/text")?.data.value; const leafletTitle = useMemo(() => { if (!firstBlockText) return "Untitled"; let doc = new Y.Doc(); const update = base64.toByteArray(firstBlockText); Y.applyUpdate(doc, update); let nodes = doc.getXmlElement("prosemirror").toArray(); return YJSFragmentToString(nodes[0]) || "Untitled"; }, [firstBlockText]); // Only handle second block logic for linear documents, not canvas let isCanvas = canvasBlocks.length > 0; let secondBlock = !isCanvas ? blocks[1] : undefined; let secondBlockTextValue = useEntity(secondBlock?.value || null, "block/text") ?.data.value; const secondBlockText = useMemo(() => { if (!secondBlockTextValue) return ""; let doc = new Y.Doc(); const update = base64.toByteArray(secondBlockTextValue); Y.applyUpdate(doc, update); let nodes = doc.getXmlElement("prosemirror").toArray(); return YJSFragmentToString(nodes[0]) || ""; }, [secondBlockTextValue]); let entitiesToDelete = useMemo(() => { let etod: string[] = []; // Only delete first block if it's a heading type if (firstBlock?.type === "heading") { etod.push(firstBlock.value); } // Delete second block if it's empty text (only for linear documents) if ( !isCanvas && secondBlockText.trim() === "" && secondBlock?.type === "text" ) { etod.push(secondBlock.value); } return etod; }, [firstBlock, secondBlockText, secondBlock, isCanvas]); return { title: leafletTitle, entitiesToDelete }; };